package city.spring.configure.security;

import city.spring.configure.security.handler.CustomLogoutRequestMatcher;
import city.spring.modules.system.service.impl.UserDetailsServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.cache.SpringCacheBasedUserCache;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
import org.springframework.security.web.context.NullSecurityContextRepository;

import javax.annotation.PostConstruct;
import java.util.Objects;

/**
 * Web安全配置
 *
 * @author HouKunLin
 * @date 2019/12/1 0001 15:55
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class CustomWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    private static final Logger logger = LoggerFactory.getLogger(CustomWebSecurityConfiguration.class);
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;
    @Autowired
    private LogoutHandler logoutHandler;
    @Autowired
    private LogoutSuccessHandler logoutSuccessHandler;
    @Autowired
    private AuthenticationSuccessHandler loginSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler loginFailureHandler;
    @Autowired
    private CustomLogoutRequestMatcher logoutRequestMatcher;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private CustomAuthenticationUserDetailsServiceImpl authenticationUserDetailsService;
    @Autowired
    private CacheManager cacheManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.cors().disable();
        http.exceptionHandling()
                // 使用安全程序Security进行授权的时候，当权限不足的时候，因权限不足不允许访问的时候，异常信息走这里设置的对象来处理
                .accessDeniedHandler(accessDeniedHandler)
                // 当未登录时，因未登录无法访问系统的时候，异常信息走这里的设置对象来处理
                .authenticationEntryPoint(authenticationEntryPoint)
        ;
        http.authorizeRequests(new CustomAuthorizeRequestsCustomizer());

        http.logout()
                .clearAuthentication(true)
                .logoutRequestMatcher(logoutRequestMatcher)
                .addLogoutHandler(logoutHandler)
                .logoutSuccessHandler(logoutSuccessHandler)
                .permitAll()
        ;
        // 自定义默认的登录程序，可以不走OAuth2方案，
        // 如果需要使用该方式登录成功的Token依旧能够使用OAuth2提供的能力，需要提供一个默认的Client信息
        // 使用自定义登录方式登录的时候，系统会有两套认证系统同时生效，这是因为安全程序会在会话中缓存一个认证信息（生效一），此时使用Token走OAuth同样也会生效（生效二）
        // 一套是OAuth2（必须提供Token来访问），此时SecurityContextHolder.getContext().getAuthentication()得到 OAuth2Authentication 对象
        // 一套是安全程序的，可以直接通过会话的方式来访问（不传Token），此时SecurityContextHolder.getContext().getAuthentication()得到 UsernamePasswordAuthenticationToken 对象
        // 其中 OAuth2Authentication 的 getDetails() 是 OAuth2AuthenticationDetails 对象
        // 其中 UsernamePasswordAuthenticationToken 的 getDetails() 是 WebAuthenticationDetails 对象
        http.formLogin()
                // 设置身份验证信息来源详细信息
                // .authenticationDetailsSource()
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
                .permitAll()
        ;
        // 启动原始安全框架表单登录后，登录成功时会把信息存入会话：L114 org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter
        // 因此覆盖这个默认的 SecurityContextRepository ，因为不覆盖的话，在创建这个对象的时候 L92 org.springframework.security.config.annotation.web.configurers.SecurityContextConfigurer.configure
        // 其 SecurityContextConfigurer L90 会自动给 SecurityContextPersistenceFilter 对象设置一个默认的 HttpSessionSecurityContextRepository 来处理会话信息
        // 因此 SecurityContextPersistenceFilter 会使用默认的 HttpSessionSecurityContextRepository 存入当前用户信息
        // 这样设置后，当使用默认的表单登录接口 /login 登录系统时，将不把用户信息存入会话中，而是像OAuth2一样返回一个同样的Token，然后只能使用这个Token来请求接口
        // 这样就达到了关闭安全程序使用会话存储信息来验证登录用户的程序，只开启OAuth2这一套认证系统
        http.securityContext().securityContextRepository(new NullSecurityContextRepository());

        // 下面这行代码不能改变默认的角色前缀信息：org.springframework.security.access.expression.SecurityExpressionRoot.defaultRolePrefix
        // 如需设置默认的角色前缀信息请看： city.spring.configure.ApplicationBeanConfiguration.grantedAuthorityDefaults
        // http.servletApi().rolePrefix("TEST_");

        // 如果未重写 protected void configure(AuthenticationManagerBuilder auth) 方法，会默认设置disableLocalConfigureAuthenticationBldr = true，
        // 此时需要在这里设置userDetailsService()，否则找不到userDetailsService对象（因为disableLocalConfigureAuthenticationBldr = true），
        // http.userDetailsService(userDetailsService());
    }

    @Override
    protected UserDetailsService userDetailsService() {
        return userDetailsService;
    }

    /**
     * 用户信息Service，因为存在另外的 UserDetailsServiceImpl、inMemoryUserDetailsManager 对象，
     * 因此加了 @Primary 注解，这样注入的时候就不用加 @Qualifier 注解了
     * 使其在自动注入的时候优先从 BeanIds.USER_DETAILS_SERVICE 获取这个对象
     *
     * @return UserDetailsService
     */
    @Primary
    @Bean(BeanIds.USER_DETAILS_SERVICE)
    @Override
    public UserDetailsService userDetailsServiceBean() {
        return userDetailsService();
    }

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager authenticationManager = super.authenticationManagerBean();
        logger.debug("认证管理器:{}", authenticationManager);
        return authenticationManager;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 这个super不能调用，因为调用了之后就不使用当前的 auth 对象来构建 AuthenticationManager 对象了，此时这里设置的 userDetailsService() 会失效不起作用
        // super.configure(auth);
        auth.userDetailsService(userDetailsService());
        // 添加这个是配合使用授权码登录时获取信息
        auth.authenticationProvider(preAuthenticatedAuthenticationProvider());
        auth.objectPostProcessor(new ObjectPostProcessor<Object>() {
            @Override
            public <O> O postProcess(O object) {
                if (object instanceof ProviderManager) {
                    ProviderManager providerManager = (ProviderManager) object;
                    providerManager.getProviders().forEach(authenticationProvider -> {
                        if (authenticationProvider instanceof DaoAuthenticationProvider) {
                            DaoAuthenticationProvider provider = (DaoAuthenticationProvider) authenticationProvider;
                            // 设置不隐藏未找到异常，当找不到用户的时候提示找不到用户，而不是提示密码错误
                            provider.setHideUserNotFoundExceptions(false);
                            // 可以在这里重新设置 i18n 资源国际化对象，可以自定义错误信息的输出
                            // provider.setMessageSource();

                            // 在这里给 DaoAuthenticationProvider 设置一个有效的用户信息缓存对象
                            // 使其在 org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate 先从缓存中获取用户信息
                            // 命中缓存的时候，不再从数据库中获取用户对象
                            Cache cache = Objects.requireNonNull(cacheManager.getCache(UserDetailsServiceImpl.CACHE_NAME));
                            SpringCacheBasedUserCache userCache = new SpringCacheBasedUserCache(cache);
                            provider.setUserCache(userCache);
                        }
                    });
                }
                return object;
            }
        });
    }

    /**
     * 在授权服务使用授权码登录时需要提供 PreAuthenticatedAuthenticationProvider 对象来处理信息
     *
     * @return PreAuthenticatedAuthenticationProvider
     */
    @Bean
    public PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() {
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(authenticationUserDetailsService);
        return provider;
    }

    @PostConstruct
    public void postConstruct() {
        logger.debug("自定义Web安全配置: {}", this);
    }
}
